package com.jprocms.module.cms.es.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.SortOptions;
import co.elastic.clients.elasticsearch._types.aggregations.*;
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.NestedQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.SearchRequest;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import co.elastic.clients.elasticsearch.core.UpdateRequest;
import co.elastic.clients.elasticsearch.core.search.HighlighterEncoder;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.SourceConfig;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.util.ObjectBuilder;
import com.google.common.collect.Lists;
import com.jpro.framework.common.pojo.CommonResult;
import com.jpro.framework.common.pojo.PageParam;
import com.jpro.framework.common.pojo.PageResult;
import com.jpro.framework.common.util.collection.MapUtils;
import com.jpro.module.infra.api.file.FileApi;
import com.jprocms.module.cms.config.JproProperties;
import com.jprocms.module.cms.controller.admin.content.vo.ContentIndexRecordDto;
import com.jprocms.module.cms.controller.admin.content.vo.ContentPageReqVO;
import com.jprocms.module.cms.convert.content.ContentConvert;
import com.jprocms.module.cms.dal.dataobject.channel.ChannelDO;
import com.jprocms.module.cms.dal.dataobject.content.ContentDO;
import com.jprocms.module.cms.dal.dataobject.site.SiteDO;
import com.jprocms.module.cms.enums.EnumContentStatus;
import com.jprocms.module.cms.es.convert.EsContentConverter;
import com.jprocms.module.cms.es.dao.EsContentRepo;
import com.jprocms.module.cms.es.dto.*;
import com.jprocms.module.cms.es.enums.EnumContentSort;
import com.jprocms.module.cms.es.enums.EnumQueryType;
import com.jprocms.module.cms.es.po.EsContent;
import com.jprocms.module.cms.es.util.EsQueryUtil;
import com.jprocms.module.cms.es.vo.ContentIndexPageVO;
import com.jprocms.module.cms.es.vo.EsChannelContentCountVO;
import com.jprocms.module.cms.es.vo.EsContentVO;
import com.jprocms.module.cms.service.channel.ChannelService;
import com.jprocms.module.cms.service.content.ContentService;
import com.jprocms.module.cms.util.CmsContextUtil;
import com.jprocms.module.cms.util.EnvUtil;
import com.jprocms.module.cms.util.PdfUtil;
import com.jprocms.module.cms.util.WordReader;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.jpro.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.jpro.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;
import static com.jprocms.module.cms.dal.dataobject.content.ContentDO.*;

/**
 * @author jprocms
 */
@Service
@Slf4j
public class ContentEsServiceImpl implements ContentEsService {

    private static List<String> fields = Arrays.stream(EsContentVO.class.getDeclaredFields())
            .map(Field::getName).collect(Collectors.toList());
    private static List<String> INDEX_FIELDS = Lists.newArrayList("id", "siteId", "channelId",
            "title", "releaseTime", "indexTime");
    @Resource
    private ElasticsearchClient elasticsearchClient;
    @Resource
    private String contentIndex;
    @Autowired
    private ChannelService channelService;
    @Resource
    private ContentService contentService;
    @Resource
    private EsContentRepo esContentRepo;
    @Resource
    private FileApi fileApi;
    @Resource
    private JproProperties jproProperties;

    @Override
    public List<EsContentVO> queryByCustomFields(Map<String, Object> customFields) throws IOException {
        if (MapUtil.isEmpty(customFields)) {
            return Collections.emptyList();
        }
        List<Query> queries = customFields.entrySet().stream().map(en -> EsQueryUtil.generateContentAdminQuery(
                        new EsQueryUtil.QueryEntity(EsQueryUtil.getExtendFiled(en.getKey()),
                                EnumQueryType.match, en.getValue()))).filter(Objects::nonNull)
                .collect(Collectors.toList());
        SearchRequest request = new SearchRequest.Builder().index(contentIndex)
                .query(new Query.Builder().nested(new NestedQuery.Builder().query(
                                q1 -> q1.bool(new BoolQuery.Builder().must(queries).build())).path("customs")
                        .build()).build()).source(b -> b.filter(f -> f.includes(
                        Stream.of("id", "typeId", "status", "title", "customFields")
                                .collect(Collectors.toList())))).build();
        log.info("query:{}", request.query());
        SearchResponse<EsContentVO> searchResponse = elasticsearchClient.search(request,
                EsContentVO.class);
        return searchResponse.hits().hits().stream().map(h -> h.source())
                .collect(Collectors.toList());
    }

    @Override
    public PageResult<ContentIndexRecordDto> getContentPageAmin(ContentAdminQueryDto queryDto) throws IOException {
        if (queryDto == null) {
            return new PageResult<>(0L);
        }
        Query query = EsQueryUtil.generateContentAdminQuery(queryDto);
        EsQueryUtil.QueryPage page = EsQueryUtil.generateQueryPage(queryDto.getPageSize().longValue(), queryDto.getPageNo().longValue(), null, null);
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query)
                .source(b -> b.filter(f -> f.includes(INDEX_FIELDS))).from(page.getFrom())
                .size(page.getSize()).sort(page.getSortOptions()).build();
        SearchResponse<ContentIndexRecordDto> searchResponse = elasticsearchClient.search(request, ContentIndexRecordDto.class);
        long totalSize = searchResponse.hits().total().value();
        List<ContentIndexRecordDto> list = searchResponse.hits().hits().stream().map(h -> h.source()).collect(Collectors.toList());
        long totalPage = 0L;
        if (totalSize > 0) {
            totalPage = new Double(Math.ceil(totalSize * 1.0 / queryDto.getPageSize())).longValue();
        }
        return new PageResult<>(list, totalSize, totalPage, queryDto.getPageNo().longValue());
    }

    @Override
    public void update(ContentEsDto contentEsDto) throws IOException {
        if (contentEsDto == null) {
            return;
        }
        if (contentEsDto.getId() == null) {
            log.warn("更新接口，id为null");
            return;
        }
        EsContent content = EsContentConverter.INSTANCE.convert(contentEsDto);
        UpdateRequest request = new UpdateRequest.Builder<>().index(contentIndex)
                .id(content.getId()).doc(content).source(c -> c.fetch(false)).build();
        elasticsearchClient.update(request, EsContent.class);

    }

    @Override
    public PageResult<EsContentVO> getContentPageForFront(EsFrontSearchDto dto) throws IOException {
        if (dto == null) {
            return null;
        }
        final List<ChannelDO> notAllowSearchChannelList = channelService.getNotAllowSearchChannelList(dto.getSiteId());
        final List<Long> notAllowSearchChannelIds = com.jpro.framework.common.util.collection.CollectionUtils.convertList(notAllowSearchChannelList, ChannelDO::getId);
        int productGrade = EnvUtil.getGrade();
        if (productGrade >= 2) {
            dto.setDocSearchSupport(jproProperties.isDocSearchSupport());
        } else {
            dto.setDocSearchSupport(false);
        }
        Query query = EsQueryUtil.generateContentAdminQuery(dto, notAllowSearchChannelIds);
        PageResult<EsContentVO> pageResult = queryPage(dto, query);
//        EsContentQueryResultVO ans = new EsContentQueryResultVO();
//        ans.setContents(pageResult);
        //暂时不做栏目聚合数量统计
//        List<EsChannelContentCountVO> channelContentCount = new ArrayList<>();
//        if (pageResult.getTotal() > 0) {
//            channelContentCount = channelContentStatistics(query);
//        }
//        ans.setChannelContentCount(channelContentCount);
        Map<Long, String> channelIdNameMap = new HashMap<>();
        List<EsContentVO> esContentVOList = pageResult.getList();
        List<Long> channelIds = com.jpro.framework.common.util.collection.CollectionUtils.convertList(esContentVOList, EsContentVO::getChannelId);
        if (CollUtil.isNotEmpty(channelIds)) {
            List<ChannelDO> channels = channelService.getChannelList(channelIds);
            CollUtil.toMap(channels, channelIdNameMap, ChannelDO::getId, ChannelDO::getName);
            //补充url和栏目名称
            fillChannelNameAndUrls(esContentVOList, channelIdNameMap);
        }
        return pageResult;
    }

    @Override
    public void resetContentIndex(ContentIndexResetDto dto) {
        ContentPageReqVO reqVO = new ContentPageReqVO();
        reqVO.setSiteId(dto.getSiteId());
        reqVO.setChannelId(dto.getChannelId());
        reqVO.setStatus(dto.getStatus());
        reqVO.setPageNo(1);
        doResetContentIndex(reqVO);
    }

    private void doResetContentIndex(ContentPageReqVO reqVO) {
        PageResult<ContentDO> contentPage = contentService.getContentPage(reqVO);
        List<ContentDO> list = contentPage.getList();
        long contentCount = 0L;
        final SiteDO site = CmsContextUtil.getSite();
        while (list.size() > 0) {
            contentCount += list.size();
            log.info("内容索引重新生成过程中,总条数 {} pageNo-{} listSize->{} ", contentPage.getTotal(), reqVO.getPageNo(), list.size());
            doResetContentIndex(list);
            reqVO.setPageNo(reqVO.getPageNo() + 1);
            contentPage = contentService.getContentPage(reqVO);
            list = contentPage.getList();
            log.info("内容索引重新生成过程中,已经生成条数 {}", contentCount);
        }
    }

    private void doResetContentIndex(List<ContentDO> list) {
        List<EsContent> esContents = new ArrayList<>();
        int productGrade = EnvUtil.getGrade();
        for (ContentDO contentDO : list) {
            ContentEsDto contentEsDto = ContentConvert.INSTANCE.convertToEsDtoSimple(contentDO, contentDO.getExt(), contentDO.getCount());
            //根据文档url解析拿到结果设置到docs
            //当前是支持文件搜索功能并且是已发布状态才去解析文档内容
            if (productGrade >= 2 && jproProperties.isDocSearchSupport() && contentDO.isPublish()) {
                final List<String> docUrls = contentDO.getDocUrls();
                if (CollUtil.isNotEmpty(docUrls)) {
                    List<String> docs = new ArrayList<>();
                    for (String docUrl : docUrls) {
                        docs.add(getDocText(docUrl));
                    }
                    contentEsDto.setDocs(docs);
                }
            }
            EsContent esContent = EsContentConverter.INSTANCE.convert(contentEsDto);
            esContent.setIndexTime(DateUtil.date(LocalDateTimeUtil.now()));
            esContent.setCustoms(MapUtils.filterMapBlank(esContent.getCustoms()));
            esContents.add(esContent);
        }
        esContentRepo.saveAll(esContents);
        esContents.clear();
    }


    private void fillChannelNameAndUrls(List<EsContentVO> records, Map<Long, String> channelIdNameMap) {
        if (CollectionUtils.isEmpty(records)) {
            return;
        }
        List<Long> ids = com.jpro.framework.common.util.collection.CollectionUtils.convertList(records, EsContentVO::getId);
        List<ContentDO> contentList = contentService.getContentList(ids, null);
        Map<Long, String> contentUrlMap = com.jpro.framework.common.util.collection.CollectionUtils.convertMap(contentList, ContentDO::getId, ContentDO::getUrl);
        Map<Long, String> contentDyUrlMap = com.jpro.framework.common.util.collection.CollectionUtils.convertMap(contentList, ContentDO::getId, ContentDO::getDynamicUrl);
        Map<Long, ChannelDO> contentChannelMap = com.jpro.framework.common.util.collection.CollectionUtils.convertMap(contentList, ContentDO::getId, ContentDO::getChannel);
        Map<Long, ContentDO> contentMap = com.jpro.framework.common.util.collection.CollectionUtils.convertMap(contentList, ContentDO::getId, c->c);
        records.forEach(r -> {
            r.setUrl(contentUrlMap.get(r.getId()));
            r.setChannelName(channelIdNameMap.get(r.getChannelId()));
            r.setDynamicUrl(contentDyUrlMap.get((r.getId())));
            r.setChannel(contentChannelMap.get(r.getId()));
            r.setOutLink(contentMap.get(r.getId()).getOutLink());
            r.setText(contentMap.get(r.getId()).getText());
        });
    }

    private List<EsChannelContentCountVO> channelContentStatistics(Query query) throws IOException {
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query)
                .aggregations("channelId", new Aggregation.Builder().terms(
                        new TermsAggregation.Builder().field("channelId").build()).build()).size(0)
                .build();
        SearchResponse<EsContentVO> searchResponse1 = elasticsearchClient.search(request,
                EsContentVO.class);
        Aggregate aggregate = searchResponse1.aggregations().get("channelId");
        List<EsChannelContentCountVO> channelContentCntDtos = new ArrayList<>();
        if (aggregate != null) {
            Object agg = aggregate._get();
            if (agg instanceof LongTermsAggregate) {
                LongTermsAggregate longTermsAggregate = (LongTermsAggregate) agg;
                Buckets<LongTermsBucket> buckets = longTermsAggregate.buckets();
                if (buckets != null) {
                    List<LongTermsBucket> bucketLists = buckets.array();
                    channelContentCntDtos = bucketLists.stream().map(b -> {
                        EsChannelContentCountVO cntDto = new EsChannelContentCountVO();
                        cntDto.setContentCount(b.docCount());
                        cntDto.setChannelId(Long.valueOf(b.key()));
                        return cntDto;
                    }).collect(Collectors.toList());
                }
            }
        }
        return channelContentCntDtos;
    }

    @Override
    public List<EsContentVO> getContentRelation(EsRelationContentQueryDto dto)
            throws IOException {
        if (dto == null) {
            return Collections.emptyList();
        }
        Query query = EsQueryUtil.generateContentAdminQuery(dto);
        EsQueryUtil.QueryPage page = EsQueryUtil.generateQueryPage(Long.valueOf(dto.getCount()), 1L,
                dto.getOrderBy(), null);
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query)
                .source(b -> {
                    ObjectBuilder<SourceConfig> filter = b.filter(f -> f.includes(fields));
                    return filter;
                }).from(page.getFrom())
                .size(page.getSize()).sort(page.getSortOptions()).build();
        SearchResponse<EsContentVO> searchResponse = elasticsearchClient.search(request, EsContentVO.class);
        List<EsContentVO> list = searchResponse.hits().hits().stream().map(h -> h.source()).collect(Collectors.toList());
        return list;
    }

    @Override
    public EsContentVO getContentNext(EsNextContentDto dto) throws IOException {
        if (dto == null) {
            return null;
        }
        Long contentId = dto.getContentId();
        EsContentVO esContentVO = queryByContentId(contentId);
        if (esContentVO == null) {
            return null;
        }

        Query query = EsQueryUtil.generateContentAdminQuery(dto, esContentVO);
        EsQueryUtil.QueryPage page = EsQueryUtil.generateNextQueryPage(1L, 1L, dto.getIsNext(), dto.getTopLevel());
        List<SortOptions> sortOptions = page.getSortOptions();
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query).source(b -> b.filter(f -> f.includes(fields))).from(page.getFrom()).size(page.getSize()).sort(sortOptions).build();
        SearchResponse<EsContentVO> searchResponse = elasticsearchClient.search(request, EsContentVO.class);
        List<EsContentVO> list = searchResponse.hits().hits().stream().map(h -> h.source()).collect(Collectors.toList());
        return CollectionUtils.isEmpty(list) ? null : list.get(0);
    }

    @Override
    public PageResult<ContentIndexPageVO> getContentPageFromEs(ContentSearchQueryDto dto, PageParam pageable) throws IOException {
        if (dto == null) {
            return new PageResult<>();
        }
        if (pageable == null) {
            pageable = new PageParam();
        }
        Query query = EsQueryUtil.generateContentAdminQuery(dto);
        EsQueryUtil.QueryPage page = EsQueryUtil.generateQueryPage(Long.valueOf(pageable.getPageSize()), Long.valueOf(pageable.getPageNo()), EnumContentSort.RELEASE_TIME_DESC.getSort(), null);
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query)
                .source(b -> b.filter(f -> f.includes(INDEX_FIELDS))).from(page.getFrom()).size(page.getSize())
                .sort(page.getSortOptions()).build();
        SearchResponse<ContentIndexPageVO> searchResponse = elasticsearchClient.search(request,
                ContentIndexPageVO.class);
        List<ContentIndexPageVO> list = searchResponse.hits().hits().stream()
                .map(h -> h.source()).collect(Collectors.toList());
        long totalSize = searchResponse.hits().total().value();
        PageResult<ContentIndexPageVO> pageResult = new PageResult<>();
        pageResult.setList(list);
        pageResult.setTotal(totalSize);
        return pageResult;
    }

    @NotNull
    private PageResult<EsContentVO> queryPage(EsFrontSearchDto dto, Query query) throws IOException {
        EsQueryUtil.QueryPage page = EsQueryUtil.generateQueryPage(dto.getPageSize(), dto.getPageNo(),
                dto.getOrderBy(), dto.getOffset());

        SearchRequest.Builder searchRequestBuilder = new SearchRequest.Builder();
        final SearchRequest.Builder queryBuilder = searchRequestBuilder.index(contentIndex).query(query);
        SearchRequest.Builder sourceBuilder = queryBuilder
                .source(b -> {
                    ObjectBuilder<SourceConfig> filter = b.filter(f -> f.includes(fields));
                    return filter;
                });
        //高亮设置
        sourceBuilder.highlight(highlightBuilder -> highlightBuilder
                .fragmentSize(3000)
                .preTags("<em>")
                .postTags("</em>").encoder(HighlighterEncoder.Default)
                .requireFieldMatch(false) //多字段时，需要设置为false
                .fields("title", highlightFieldBuilder -> highlightFieldBuilder)
                .fields("txts.txt", highlightFieldBuilder -> highlightFieldBuilder)
        );
        SearchRequest request = sourceBuilder.from(page.getFrom()).size(page.getSize()).sort(page.getSortOptions()).build();

        SearchResponse response = elasticsearchClient.search(request, EsContentVO.class);
        final TotalHits total = response.hits().total();
        final List<Hit> hits = response.hits().hits();
        List<EsContentVO> list = new ArrayList<>();
        for (Hit<EsContentVO> hit : hits) {
            EsContentVO esContentVO = hit.source();
            final List<String> highlightTitles = hit.highlight().get("title");
            final List<String> highlightTxts = hit.highlight().get("txts.txt");
            if (CollUtil.isNotEmpty(highlightTitles)) {
                esContentVO.setHighlightTitle(highlightTitles.get(0));
            }
            if (CollUtil.isNotEmpty(highlightTxts)) {
                esContentVO.setHighlightTxt(Jsoup.clean(highlightTxts.get(0),Safelist.none().addTags("em")));
            }
            list.add(esContentVO);
        }
        //没有命中正文的将部分正文去除html返回
        List<EsContentVO> filterNoHighlightContentVOS = list.stream().filter(c -> StringUtils.isEmpty(c.getHighlightTxt())).collect(Collectors.toList());
        if (CollUtil.isNotEmpty(filterNoHighlightContentVOS)) {
            final List<Long> filterNoHighlightContentIds = com.jpro.framework.common.util.collection.CollectionUtils.convertList(filterNoHighlightContentVOS, EsContentVO::getId);
            final List<ContentDO> contentList = contentService.getContentList(filterNoHighlightContentIds, EnumContentStatus.PUBLISHED.getStatus());
            for (int i = 0; i < filterNoHighlightContentVOS.size(); i++) {
                final EsContentVO contentVO = filterNoHighlightContentVOS.get(i);
                if (contentList.size() > i) {
                    final ContentDO contentDO = contentList.get(i);
                    if (contentDO != null) {
                        contentVO.setHighlightTxt(contentDO.getPlainText());
                    }
                }
            }
        }
        PageResult<EsContentVO> pageResult = new PageResult<>();
        pageResult.setList(list);
        long totalSize = total.value();
        pageResult.setTotal(totalSize);
        if (totalSize > 0) {
            pageResult.setPages(new Double(Math.ceil(totalSize * 1.0 / dto.getPageSize())).longValue());
        } else {
            pageResult.setPages(0L);
        }
        pageResult.setCurrent(dto.getPageNo());
        return pageResult;
    }

    private EsContentVO queryByContentId(Long contentId) throws IOException {
        Query query = EsQueryUtil.generateContentAdminQuery(new EsQueryUtil.QueryEntity("id", EnumQueryType.term, contentId));
        SearchRequest request = new SearchRequest.Builder().index(contentIndex).query(query).source(b -> b.filter(f -> f.includes(fields))).build();
        SearchResponse<EsContentVO> searchResponse = elasticsearchClient.search(request, EsContentVO.class);
        List<EsContentVO> list = searchResponse.hits().hits().stream().map(h -> h.source()).collect(Collectors.toList());
        return CollectionUtils.isEmpty(list) ? null : list.get(0);
    }

    @Override
    public String getDocText(String url) {
        // 图片裁剪。图片任意裁剪，生成新图片。
        String extension = FilenameUtils.getExtension(url);
        // 读取内容
        final CommonResult<byte[]> fileContent = fileApi.getFileContent(url);
        byte[] content;
        try {
            content = fileContent.getData();
        } catch (Exception e) {
            //文件删除返回空字符串，不抛出异常
            return StrUtil.EMPTY;
        }
        if (content == null) {
            //文件删除返回空字符串，不抛出异常
            return StrUtil.EMPTY;
        }
        //从数据库记录拿到文件
        File tempFile = null;
        try {
            try {
                tempFile = Files.createTempFile(null, "." + extension).toFile();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            FileUtil.writeBytes(content, tempFile);
            if (tempFile == null && !tempFile.exists()) {
                throw exception(FILE_NOT_EXISTS);
            }
            if (DOCUMENT_DOC.endsWith(extension)) {
                return WordReader.getDocText(tempFile);
            } else if (DOCUMENT_DOCX.endsWith(extension)) {
                return WordReader.getDocxText(tempFile);
            } else if (DOCUMENT_PDF.endsWith(extension)) {
                return PdfUtil.getPdfText(tempFile);
            }
        } finally {
            if (tempFile != null && tempFile.exists()) {
                tempFile.delete();
            }
        }
        return null;
    }
}
